import sys
import os
import botconfig
from tools import decorators
from urllib.request import urlopen
from urllib.parse import quote
from urllib.error import HTTPError
import json
import re
from oyoyo.parse import parse_nick
import fnmatch
import csv
import io
import settings.stocks_settings as var
import sqlite3
import locale
import threading
import socket
import collections
from base64 import b64encode, b64decode
import zlib

try:
    import feedparser
except ImportError:
    print("You need feedparser to enable RSS feeds!")
    feedparser = None

COMMANDS = {}
PM_COMMANDS = {}
HOOKS = {}

unhook = lambda hid: decorators.unhook(HOOKS, hid)

cmd = decorators.generate(COMMANDS)
pmcmd = decorators.generate(PM_COMMANDS)
hook = decorators.generate(HOOKS, raw_nick=True, permissions=False)

SYMBOL_LOOKUP_URL = "http://autoc.finance.yahoo.com/autoc?query={0}&callback=YAHOO.Finance.SymbolSuggest.ssCallback"
TICKER_LOOKUP_URL = "http://finance.yahoo.com/d/quotes.csv?s={0}&f={1}"
GOOG_URL = "http://www.google.com/finance/info?infotype=infoquoteall&q={0}"
SHORTEN_URL = "https://api-ssl.bitly.com/v3/shorten?login={login}&apiKey={apikey}&longUrl={0}"

def quit_callback(cli):
    if var.RSS_THREAD and var.RSS_THREAD.is_alive():
        var.RSS_THREAD.cancel()
        
        
@pmcmd("feedstatus", admin_only = True)
def get_feed_status(cli, nick, rest):
    """Get the status of the RSS feed"""
    if var.RSS_THREAD and var.RSS_THREAD.is_alive():
        cli.msg(nick, "The RSS functions are up and running.")
    else:
        cli.msg(nick, "The RSS functions are not running.")


@pmcmd("restart", admin_only=True)
@cmd("restart", admin_only=True)
def restart_program(cli, nick, *rest):
    """Restarts the bot."""
    try:
        quit_callback(cli)
        cli.quit("Forced restart from "+ nick)
        raise SystemExit
    finally:
        print("RESTARTING")
        python = sys.executable
        if rest[-1].strip().lower() == "debugmode":
            os.execl(python, python, sys.argv[0], "--debug")
        elif rest[-1].strip().lower() == "normalmode":
            os.execl(python, python, sys.argv[0])
        elif rest[-1].strip().lower() == "verbosemode":
            os.execl(python, python, sys.argv[0], "--verbose")
        else:
            os.execl(python, python, *sys.argv)

        
@pmcmd("die", "bye", admin_only=True)
@cmd("die", "bye", admin_only=True)
def forced_exit(cli, nick, *rest):  # Admin Only
    """Forces the bot to close"""
    quit_callback(cli)
    cli.quit("Forced quit from "+nick)
    raise SystemExit
    
    

    
@cmd("search", "symbol")
def lookup_symbol(cli, nick, chan, rest):
    """Looks up the ticker symbol of a stock"""
    rest = rest.strip()
    if not rest:
        cli.notice(nick, "Not enough parameters.")
        return
    with urlopen(SYMBOL_LOOKUP_URL.format(quote(rest.strip()))) as fl:
        data = fl.read().strip()
        if not data:
            cli.notice(nick, "Something went wrong with Yahoo Finance.")
            return
        data = data.decode("utf_8", errors='ignore')
        data = data[data.index("{"):data.rindex(")")]  # strip out the non-json parts
        
        results = json.JSONDecoder().decode(data)["ResultSet"]["Result"]
        if not results:
            cli.msg(chan, "Not a valid stock.")
            return
        result = results[0]
        cli.msg(chan, "Symbol for {name}: \x02{symbol}\x02".format(**result))


def is_admin(cloak):
    return bool([ptn for ptn in botconfig.OWNERS+botconfig.ADMINS 
        if fnmatch.fnmatch(cloak.lower(), ptn.lower())])
    

@pmcmd("help", raw_nick = True)
def get_help(cli, rnick, rest):
    """Gets a list of commands or gets help for a specific command."""
    nick, mode, user, cloak = parse_nick(rnick)
    fns = []

    rest = rest.strip().replace(botconfig.CMD_CHAR, "", 1).lower()
    splitted = re.split(" +", rest, 1)
    cname = splitted.pop(0)
    rest = splitted[0] if splitted else ""
    found = False
    if cname:
        for c in (COMMANDS,PM_COMMANDS):
            if cname in c.keys():
                found = True
                for fn in c[cname]:
                    if fn.__doc__:
                        if callable(fn.__doc__):
                            cli.msg(nick, botconfig.CMD_CHAR+cname+": "+fn.__doc__(rest))
                        else:
                            cli.msg(nick, botconfig.CMD_CHAR+cname+": "+fn.__doc__)
                        return
                    else:
                        continue
                else:
                    continue
        else:
            if not found:
                cli.msg(nick, "Command not found.")
            else:
                cli.msg(nick, "Documentation for this command is not available.")
            return
    # if command was not found, or if no command was given:
    for name, fn in COMMANDS.items():
        if (name and not fn[0].admin_only and 
            not fn[0].owner_only and name not in fn[0].aliases):
            fns.append("\u0002"+name+"\u0002")
    afns = []
    if is_admin(cloak) or cloak in botconfig.OWNERS: # todo - is_owner
        for name, fn in COMMANDS.items():
            if fn[0].admin_only and name not in fn[0].aliases:
                afns.append("\u0002"+name+"\u0002")
    cli.notice(nick, "Commands: "+", ".join(fns))
    if afns:
        cli.notice(nick, "Admin Commands: "+", ".join(afns))

                  
def get_realtime_quote_from_goog(*symbol):
    try:
        with urlopen(GOOG_URL.format(quote(",".join(symbol)))) as fil:
            data = fil.read().decode("utf-8", errors="ignore")
            if not data.strip():
                return []

            if data.strip().startswith("// "):
                data = data.strip()[3:]

            def myfunc(mo):
                try:
                    return chr(int(mo.group(0)[2:], 16))
                except:
                    return ""  # ignore error!
                    
                    
            data = re.sub(r"\\x[A-F0-9a-f][A-F0-9a-f]", myfunc, data)
            jsn = json.JSONDecoder().decode(data)
            if not jsn:
                return {}
            retlst = []
            for dcd in jsn:
                ret = {}
                ret["l"] = dcd.pop("l_cur", None)
                ret["symbol"] = dcd.pop("t", None)
                ret["name"] = dcd.pop("name", None)
                ret["change"] = dcd.pop("c", None)
                ret["changep"] = dcd.pop("cp", None)
                ret["low"] = dcd.pop("lo", None)
                ret["high"] = dcd.pop("hi", None)
                ret["afterhoursl"] = dcd.pop("el_cur", None)
                ret["ahchange"] = dcd.pop("ec", None)
                ret["ahchangep"] = dcd.pop("ecp", None)
                ret["vol"] = dcd.pop("vo", None)
                ret["avol"] = dcd.pop("avvo", None)
                ret["mcap"] = dcd.pop("mc", None)
                if ret["changep"]:
                    ret["changep"] += "%"
                if ret["ahchangep"]:
                    ret["ahchangep"] += "%"
                retlst.append(ret)
            return retlst
    except HTTPError:
        return []
    
                  
def get_yahoo_quote(*symb):
    yahoo_values = ("l", "change", "low", "high", "changep", "vol", "avol", "mcap")
    fl = urlopen(TICKER_LOOKUP_URL.format(quote("+".join(symb)), "l1c1ghp2va2j1"))
    data = fl.read().decode("utf_8", errors='ignore')
    if not data.strip():
        cli.notice(nick, "Something went wrong with Yahoo Finance.")
        return
    rder = csv.reader(io.StringIO(data))
    reslt = []
    for row in rder:
        quot = dict()
        if len(row) == len(yahoo_values):
            for i,x in enumerate(yahoo_values):
                quot[x] = row[i]
            reslt.append(quot)
    return reslt 

@cmd("q", "stock", "quote", "lookup", "ticker")
def ticker(cli, nick, chan, rest, useyahoo = False):
    """Looks up information about a stock"""
    rest = re.split(" +",rest)[0].strip()
    if not rest:
        cli.notice(nick, "Not enough parameters.")
        return
    if not useyahoo:
        quot = get_realtime_quote_from_goog(rest)
    else:
        quot = get_yahoo_quote(rest)
            
    if quot:
        quot = quot[0]
        c_clr = "05" if quot["change"].startswith("-") else "03"
        afterhrs = ("[AH: \02{0}\02] ".format(quot["afterhoursl"])
                    if quot.get("afterhoursl") else "")
        ahc = ""
        if quot.get("ahchange"):
            num = "\02\03{clr}{0} ({1})\03\02".format(quot["ahchange"], quot["ahchangep"],
                    clr = "05" if quot["ahchange"].startswith("-") else "03")
            ahc = "[AH: {0}] ".format(num)
        if not (quot["low"] and quot["high"]):
            rng = "N/A"
        else:
            rng = "{0} - {1}".format(quot["low"], quot["high"])
        cli.msg(chan, ("Last Trade: {b1}{l}{b1} {ah}| Change: {b1}\x03{c}{change} "+
            "({changep})\x03{b1} {ahc}| Range: {rnge}{dly}").format(
                c = c_clr,
                b1 = "" if afterhrs else "\x02",
                ah = afterhrs,
                ahc = ahc,
                rnge = rng,
                dly = " (delayed)" if useyahoo else "",
                **quot))
    else:
        cli.msg(chan, "Not a valid stock.")
        return
        
    def get_more_info(cli, nick, chan, _):
        del COMMANDS["more"]        
        msg = "Volume: {0} | Avg Daily Volume: {1} | Market Cap: {2}".format(
                quot.get("vol", "N/A") or "N/A", 
                quot.get("avol", "N/A") or "N/A", 
                quot.get("mcap", "N/A") or "N/A")
        cli.msg(chan, msg)
    if COMMANDS.get("more"):
        del COMMANDS["more"]
    cmd("more")(get_more_info)
cmd("yq")(lambda *x: ticker(x[0], x[1], x[2], x[3], True))  # yahoo      

@cmd("help", raw_nick = True)
def help2(cli, nick, chan, rest):
    """Gets help"""
    
    if rest.strip():  # command was given
        get_help(cli, chan, rest)
    else:
        get_help(cli, nick, rest)
        
@cmd("eval", owner_only = True)
@pmcmd("eval", owner_only = True)
def pyeval(cli, nick, *rest):
    rest = list(rest)
    if len(rest) == 2:
        chan = rest.pop(0)
    else:
        chan = nick
    try:
        a = str(eval(rest[0]))
        if len(a) < 500:
            cli.msg(chan, a)
        else:
            cli.msg(chan, a[0:500])
    except Exception as e:
        cli.msg(chan, str(type(e))+":"+str(e))
        
        

@cmd("exec", owner_only = True)
@pmcmd("exec", owner_only = True)
def py(cli, nick, *rest):
    rest = list(rest)
    if len(rest) == 2:
        chan = rest.pop(0)
    else:
        chan = nick
    try:
        exec(rest[0])
    except Exception as e:
        cli.msg(chan, str(type(e))+":"+str(e))

        
# start trading game stuff

Holding = collections.namedtuple("Holding", ["type", "symbol", "amount", "etc"])

def from_Holding(hdng):
    raw = hdng._asdict()
    raw_json = json.dumps(raw, separators=(',',':'))
    
    return raw_json
    
def to_Holding(raw):
    return Holding(**json.loads(raw))
    
def encode_assets(holdings):
    return zlib.compress(b64encode(bytes("\07".join(from_Holding(h) for h in holdings), "utf-8")))

def decode_assets(coded):
    result = b64decode(zlib.decompress(coded)).decode("utf-8").split("\07")
    if not result[0]:
        return []
    else:
        return [to_Holding(x) for x in result]
    
def login(nick, use_nick = False):
    # first try to login as account
    if nick not in var.USERS:
        if nick.startswith("a:") or nick.startswith("n:"):
            uname = nick
        else:
            return
    # a: means the acc is tied to a NickServ acc, n: -> just nick
    elif var.USERS[nick].acc != "*" and not use_nick:
        uname = "a:"+var.USERS[nick].acc
    else:
        uname = "n:"+nick
    
    with var.LOCK:
        conn = sqlite3.connect(botconfig.DATABASE_FILE)
        x = conn.execute(('''select * from players '''+
                          '''where user = ? limit 1'''), (uname,))
        res = x.fetchone()
        conn.cursor().close()
        
    if not res:
        if nick == uname:
            return  # calling manually
        if uname.startswith("a:"):
            return login(nick, True)
    else:
        res = list(res)
        assets = decode_assets(bytes(res[2]))
        res[2] = assets
        
    return res
    

def save_stats(uname, cash, assets):
    """For saving changes to the database."""
    for h in list(assets):
        if h.amount:
            continue

        if h.type == "short":
            cash += h.etc["credit"]
            cash += h.etc["collateral"]
            # todo: 1% broker investment bonus?
            # should it be compounded annually or what?
       
        assets.remove(h)

    enc = encode_assets(assets)
    with var.LOCK:
        conn = sqlite3.connect(botconfig.DATABASE_FILE)
        blb = sqlite3.Binary(enc)
        conn.execute('''update players set cash=?,holdings=? where user=?''', 
                        (cash, blb,uname))
        conn.commit()
        conn.cursor().close()


@cmd("addfunds", "addmoney", admin_only = True)
def add_money(cli, nick, chan, rest):
    """Add cash to an account."""
    rest = re.split(" +", rest, 1)
    if len(rest) != 2:
        cli.msg(chan, "Syntax: {0}addfunds <who> <amount>".format(botconfig.CMD_CHAR))
        return
    who, amount = rest
    try:
        amount = float(amount)
    except ValueError:
        cli.msg(chan, "Syntax: {0}addfunds <who> <amount>".format(botconfig.CMD_CHAR))
        return
    r = login(who)
    if not r:
        cli.msg(chan, "Account not found.")
        return
    uname, cash, assets = r
    cash += amount
    save_stats(uname, cash, assets)
    cli.msg(chan, "Money successfully transferred.")
        
        
@cmd("acc", "createacc", "makeacc", "newacc", "account")
def make_acc(cli, nick, chan, rest):
    """Creates a new trading game account based on your NickServ account or nickname"""
    if nick not in var.USERS:
        cli.msg(chan, "Something went wrong.  Do /cycle and try again.")
        return
    if var.USERS[nick].acc != "*":
        uname = "a:"+var.USERS[nick].acc
    else:
        uname = "n:"+nick
    
    with var.LOCK:
        conn = sqlite3.connect(botconfig.DATABASE_FILE)
        x = conn.execute(('''select exists(select 1 from players '''+
                          '''where user = ? limit 1)'''), (uname,))
        res = x.fetchone()[0]
        if res:  # found an entry already!
            cli.msg(chan, "You already have an account: \02{0}\02.".format(uname))
            return
        # make an account now
        empti = sqlite3.Binary(zlib.compress(b''))
        conn.execute('''insert into players values (?,?,?)''', 
                        (uname, var.STARTING_CASH, empti))
        conn.commit()
        conn.cursor().close()
    
    cli.msg(chan, "You have successfully set up an account.")
    
    
@cmd("stats", "mystats", "portfolio")
def get_my_stats(cli, nick, chan, rest):
    """Gets your portfolio?"""
    result = login(nick)
    if not result:
        cli.notice(nick, "Unable to retrieve stats.  You are not logged in.")
        return
    uname, cash, assets = result
    
    note = " (insecure)" if uname.startswith("n:") else ""
    msg = []
    symbols = []
    for holdn in assets:
        symb = holdn.symbol
        amt = holdn.amount
        symbols.append(symb)
        shrtnote = " short" if holdn.type == "short" else ""
        msg.append("\02{0}\02({1}{2})".format(symb, amt, shrtnote))
    msg = " ".join(msg)
    if not msg:
        msg = "none"
    
    if symbols:
        stuff = get_realtime_quote_from_goog(*symbols)
    portvalue = 0
    for i,h in enumerate(assets):
        pric = stuff[i]["afterhoursl"] if stuff[i].get("afterhoursl", 0) else stuff[i]["l"]
        pric = float(pric)
        if h.type == "short":
            pric *= -1
        portvalue += h.amount * pric
        portvalue += h.etc.get("credit", 0)
        portvalue += h.etc.get("collateral", 0)

    cli.msg(chan, ("\02{0}\02 - Cash: \02${1:.2f}\02, Stocks: {3}{2}, "+
            "Portfolio Value: \02${4:.2f}\02").format(
                    uname[2:], cash, note, msg, portvalue))
            
            
@cmd("buy", "purchase")
def buy_stock(cli, nick, chan, rest, maxposs = False):
    result = login(nick)
    if not result:
        cli.notice(nick, "Unable to complete transaction.  You are not logged in.")
        return
    uname, cash, assets = result
    rest = re.split(" +",rest,1)
    if len(rest) != 2 and not (len(rest) == 1 and maxposs):
        cli.msg(chan, "Syntax: {0}buy <SYMBOL> <number of shares>".format(botconfig.CMD_CHAR))
        return
    symb = rest[0].strip()
    if not maxposs:
        amount = rest[1].strip()
    symb = symb.upper()
    try:
        if not maxposs:
            amount = int(amount)
    except ValueError:
        cli.msg(chan, "Invalid argument for <number of shares>: "+amount)
        return
    quot = get_realtime_quote_from_goog(symb)
    if quot:
        quot = quot[0]
    else:
        cli.msg(chan, "Not a valid stock.")
        return
    if quot.get("afterhoursl"):
        c = float(quot["afterhoursl"].replace(",",""))
    elif quot.get("l"):
        c = float(quot["l"].replace(",",""))
    else:
        cli.msg(chan, "Not a valid stock.")
    if maxposs:
        amount = int(cash/c)
    if not amount:
        cli.msg(chan, "You cannot buy 0 shares.")
        return
    adv = quot.get("avol")
    if adv.endswith("M"):
        adv = float(adv[:-1])*1000000
    elif adv != "N/A":
        adv = float(adv)
    else:
        adv = 999999999
    if amount > int(0.5 * adv):
        if maxposs:
            amount = int(0.5 * adv)
        cli.msg(chan, "You can only buy up to 50% of the average daily volume.")
        return
    cost = amount * c 
    already = [i for (i, hld) in enumerate(assets) if hld.symbol == symb]
    if c < var.MINIMUM_PRICE and not (already and assets[already[0]].type == "short"):
        cli.msg(chan, "The stock price is lower than the minimum.")
        return
    if cost+var.COMMISSION < cash or (already and 
                assets[already[0]].type == "short" and
                (assets[already[0]].etc["collateral"]+
                 assets[already[0]].etc["credit"] + cash) > cost + var.COMMISSION and
                 amount == assets[already[0]].amount):
        # either they have enough cash or enough to cover the short
        cli.msg(chan, ("Your order would cost "+
                      "${0:.2f} + ${1:.2f} (commission) = ${2:.2f}.  "+
                      "Use !confirm to place your order.").format(
                      cost, var.COMMISSION, cost+var.COMMISSION))
        var.PERSON_ORDERING = nick
        
        def confirm_order(cli, nick, chan, rest):
            if nick != var.PERSON_ORDERING:
                return
            cash_r = cash - (cost+var.COMMISSION)
            
            if already and assets[already[0]].type == "long":
                i = already[0]
                assets[i] = Holding("long", symb, assets[i].amount + amount, dict())
            elif already and assets[already[0]].type == "short":
                i = already[0]
                tobuy = max(-1 * (assets[i].amount - amount), 0)
                shortleft = max(assets[i].amount - amount, 0)
                assets[i] = Holding("short", symb, shortleft, assets[i].etc)
                if tobuy:
                    assets.append(Holding("long", symb, tobuy, dict()))
            else:
                assets.append(Holding("long", symb, amount, dict()))
            
            save_stats(uname, cash_r, assets)
            cli.notice(nick, "Your order has been placed.")
            del COMMANDS["confirm"]
            return
        if COMMANDS.get("confirm"):
            del COMMANDS["confirm"]
        cmd("confirm")(confirm_order)
        
        return
    else:
        cli.msg(chan, ("Your order would cost {0:.2f}.  "+
                "You do not have enough money.").format(cost))
        return
        

@cmd("buymax", "buyall")
def buy_max(cli, nick, chan, rest):
    """Spend all your cash buying as many stocks as you can"""
    buy_stock(cli, nick, chan, rest, True)
        

@cmd("short", "shortsell")
def sell_short(cli, nick, chan, rest):
    result = login(nick)
    if not result:
        cli.notice(nick, "Unable to complete transaction.  You are not logged in.")
        return
    uname, cash, assets = result
    rest = re.split(" +",rest,1)
    if len(rest) != 2:
        cli.msg(chan, "Syntax: {0}short <SYMBOL> <number of shares>".format(botconfig.CMD_CHAR))
        return
    symb, amount = rest[0].strip(), rest[1].strip()
    symb = symb.upper()
    
    try:
        amount = int(amount)
    except ValueError:
        cli.msg(chan, "Invalid argument for <number of shares>: "+amount)
        return
    
    indx = [i for (i, a) in enumerate(assets) if a.type == "long" and a.symbol == symb]
    # does the person have long shares of this?
    if indx:
        cli.notice(nick,("You seem to already have shares of this... "+
                "I'm not sure what to do. :("))
        return
    elif not amount:
        cli.notice(nick, "You can't do that.")
    
    quot = get_realtime_quote_from_goog(symb)
    if quot:
        quot = quot[0]
    else:
        cli.msg(chan, "Not a valid stock.")
        return
    if quot.get("afterhoursl"):
        pric = float(quot["afterhoursl"].replace(",", ""))
    elif quot.get("l"):
        pric = float(quot["l"].replace(",", ""))
    else:
        cli.msg(chan, "Not a valid stock.")
        return
    earn = pric * amount
    coll = earn * 0.5
    if pric < var.MINIMUM_PRICE_SHORT:
        cli.msg(chan, ("The price for this stock is lower than the minimum" +
                " for shorting (${0:.2f}).").format(var.MINIMUM_PRICE_SHORT))
        return
    if coll > cash - var.COMMISSION:
        cli.notice(nick, ("You do not have enough to pay for the 50% "+
                "collateral ({0}) required by the brokerage.").format(coll))
        return
    # OKAY ready to short!    
    cli.msg(chan, ("Short selling {2} shares of {0} for ${1:.2f} "+
            "(${3:.2f} collateral).  Use !confirm "+
            "to place your order.").format(symb, earn, amount, coll))
    
    var.PERSON_ORDERING = nick
    def confirm_short(cli, nick, chan, rest):
        if nick != var.PERSON_ORDERING:
            return  # no steal confirmations!
        for i,hld in enumerate(assets):  # time to update assets
            if hld.type == "short" and hld.symbol == symb:
                hld.etc["collateral"] += coll
                hld.etc["credit"] += earn
                assets[i] = Holding("short", symb, hld.amount + amount,
                        hld.etc["collateral"])
                cashr = cash - coll - var.COMMISSION
                
                save_stats(uname, cashr, assets)
                break
        else:
            etc = {"collateral" : coll,
                   "credit"     : earn}
            assets.append(Holding("short", symb, amount, etc))
            
            cashr = cash - coll - var.COMMISSION
            
            save_stats(uname, cashr, assets)
        cli.notice(nick, "Transaction successful.")
        del COMMANDS["confirm"]
        return
    if "confirm" in COMMANDS:
        del COMMANDS["confirm"]
    cmd("confirm")(confirm_short)
                
    
    
        
        
@cmd("sell", "sellstock", "sellstocks")
def sell_stock(cli, nick, chan, rest):
    result = login(nick)
    if not result:
        cli.notice(nick, "Unable to complete transaction.  You are not logged in.")
        return
    uname, cash, assets = result
    rest_old = rest
    rest = re.split(" +",rest,1)
    
    if not rest[0]:
        cli.msg(chan, "Syntax: {0}sell <SYMBOL> <number of shares>".format(botconfig.CMD_CHAR))
        return
    
    symb = rest[0].upper()
    indx = [i for (i, a) in enumerate(assets) if a.type == "long" and a.symbol == symb]
    
    if len(rest) == 1:
        if not indx:
            cli.msg(chan, "Syntax: {0}sell <SYMBOL> <number of shares>".format(botconfig.CMD_CHAR))
            return
        amount = assets[indx[0]].amount  # sell all shares of this one
    else:
        amount = rest[1].strip()
    try:
        amount = int(amount)
    except ValueError:
        cli.msg(chan, "Invalid argument for <number of shares>: "+amount)
        return
    if not indx:
        sell_short(cli, nick, chan, rest_old)
        return
    holding = assets[indx[0]]
    if amount > holding.amount:
        cli.msg(chan, ("You do not have {0} shares of {1} to sell.  "+
                "You only have {2} shares.").format(amount, symb, holding.amount))
        return
    elif not amount:
        cli.msg(chan, "You cannot do that.")
        return
    quot = get_realtime_quote_from_goog(symb)
    if quot:
        quot = quot[0]
    else:
        cli.msg(chan, "Not a valid stock.")
        return
    if quot.get("afterhoursl"):
        earn = amount * float(quot["afterhoursl"].replace(",", ""))
    elif quot.get("l"):
        earn = amount * float(quot["l"].replace(",", ""))
    else:
        cli.msg(chan, "Not a valid stock.")
        return
    cli.msg(chan, ("Selling {2} shares of {0} for ${1:.2f}.  Use !confirm "+
                    "to place your order.").format(symb, earn, amount))

    var.PERSON_ORDERING = nick
    def confirm_order(cli, nick, chan, rest):
        if nick != var.PERSON_ORDERING:
            return
        cash_r = cash + (earn-var.COMMISSION)
        assets[indx[0]] = Holding("long", symb, 
                assets[indx[0]].amount - amount, assets[indx[0]].etc)
        
        save_stats(uname, cash_r, assets)
        cli.notice(nick, "Transaction successful.")
        del COMMANDS["confirm"]
        return
    if COMMANDS.get("confirm"):
        del COMMANDS["confirm"]
    cmd("confirm")(confirm_order)
    

@cmd("resetacc", "reset")
def reset_acc(cli, nick, chan, rest):
    result = login(nick)
    if not result:
        cli.notice(nick, "Unable to complete transaction.  You are not logged in.")
        return
    uname, cash, assets = result
    
    cli.msg(chan, ("For a fee of ${0:.2f}, you may sell all of "+
            "your holdings for a total of ${1:.2f}.  "+
            "Are you sure you want to do this?  "+
            "Use !yes to confirm.").format(cash, var.STARTING_CASH))
    
    var.PERSON_RESETTING = nick
    def confirm_reset(cli, nick, chan, rest):
        if nick != var.PERSON_RESETTING:
            return
        cash = var.STARTING_CASH
        
        save_stats(uname, cash, [])
        cli.msg(chan, "Your account has been reset.")
        del COMMANDS["yes"]
        return
    if COMMANDS.get("yes"):
        del COMMANDS["yes"]
    cmd("yes")(confirm_reset)
    
    
    
@hook("join")
def on_join(cli, raw_nick, chan, acc="*", rname=""):
    nick,m,u,cloak = parse_nick(raw_nick)
    if nick not in var.USERS.keys() and nick != botconfig.NICK:
        var.USERS[nick] = User(acc)
    if acc != "*":
        uname = "a:"+acc
    else:
        uname = "n:"+nick
    with var.LOCK:
        conn = sqlite3.connect(botconfig.DATABASE_FILE)
        x = conn.execute(('''select exists(select 1 from players '''+
                          '''where user = ? limit 1)'''), (uname,))
        if next(x)[0]:
            cli.notice(nick, "Welcome!  You are logged in.")
        x.close()


@hook("account")
def on_account(cli, nick, acc):
    nick = parse_nick(nick)[0]    
    if nick in var.USERS.keys():
        var.USERS[nick] = User(acc)


@hook("nick")
def on_nick(cli, prefix, nick):
    prefix,u,m,cloak = parse_nick(prefix)
    chan = botconfig.CHANNEL

    if prefix in var.USERS:
        var.USERS[nick] = var.USERS.pop(prefix)
        
    if prefix == var.PERSON_ORDERING:
        var.PERSON_ORDERING = nick
    if prefix == var.PERSON_RESETTING:
        var.PERSON_RESETTING = nick
    if nick in var.USERS and var.USERS[nick].acc == "*":
        with var.LOCK:
            conn = sqlite3.connect(botconfig.DATABASE_FILE)
            x = conn.execute(('''select * from players where
                    user=?'''), ("n:"+prefix,)).fetchone()
            if x:
                x = list(x)
                x[0] = "n:"+nick
                conn.execute(('''insert or ignore into players
                    values (?,?,?)'''), x)
                conn.execute(('''delete from players where
                    user=?'''), ("n:"+prefix,))
            conn.commit()
            conn.cursor().close()

def leave(cli, nick, why=""):
    nick, _, _, cloak = parse_nick(nick)
        
    if why == botconfig.CHANGING_HOST_QUIT_MESSAGE:
        return
    if nick in var.USERS:
        del var.USERS[nick]

#Functions decorated with hook do not parse the nick by default
hook("part")(lambda cli, nick, *rest: leave(cli, nick))
hook("quit")(lambda cli, nick, *rest: leave(cli, nick, rest[0]))
hook("kick")(lambda cli, nick, *rest: leave(cli, rest[1]))


# end trading game stuff

@cmd("delfeed", admin_only = True)
def del_feed(cli, nick, chan, rest):
    rest = rest.strip()
    if not rest:
        cli.msg(chan, "Syntax: {0}delfeed <feed-short-name>".format(botconfig.CMD_CHAR))
        return
    if rest not in var.FEEDS.values():
        cli.msg(chan, "Feed {0} not found".format(rest))
        return
    with var.LOCK:
        for url in list(var.FEEDS.keys()):
            if var.FEEDS[url] == rest:
                del var.FEEDS[url]
        conn = sqlite3.connect(botconfig.DATABASE_FILE)
        conn.execute('''delete from feeds where alias=?''', (rest,))
        conn.commit()
        conn.cursor().close()
    cli.msg(chan, "Operation successful.")
        
        
@cmd("addfeed", admin_only = True)
def add_feed(cli, nick, chan, rest):
    """!addfeed <feed-url> <feed-short-name>"""
    
    rest = re.split(" +",rest,1)
    if len(rest) != 2:
        cli.msg(chan, "Syntax: {0}addfeed <feed-url> <feed-short-name>".format(botconfig.CMD_CHAR))
        return
    feed, alias = rest
    feed = feed.strip()
    alias = alias.strip()
    if not re.match("\S+", alias):
        cli.msg(chan, "Syntax: {0}addfeed <feed-url> <feed-short-name>".format(botconfig.CMD_CHAR))
        return
    # refactor this:
    with var.LOCK:
        conn = sqlite3.connect(botconfig.DATABASE_FILE)
        try:
            dataa = urlopen(feed, timeout = 7).read().decode("utf_8", "replace")
            parsed = feedparser.parse(dataa)
        except HTTPError as e:
            cli.msg(chan, ("Get feed failed with error: {0}.  "+
                    "Please check the link or try again later.").format(e))
            return
        except socket.error as e:
            cli.msg(chan, "Some socket error happened.  Try again.")
            return
        except socket.timeout:
            cli.msg(chan, "Connection timed out.  Try again.")
            return
        if parsed["bozo"]:
            cli.msg(chan, "Encountered invalid feed error "+str(parsed["bozo_exception"]))
            return
        if not parsed["items"]:
            cli.msg(chan, "Feed is empty.  Please try again later.")
            return
        conn.execute('''insert or replace into feeds (url,alias) values (?,?)''', (feed, alias))
        var.FEEDS[feed] = alias
        for item in parsed["items"]:
            conn.execute('''insert or ignore into seen values (?,?)''', (item["link"], feed))
        conn.commit()
        conn.cursor().close()
        
        var.INITED_FEEDS.add(feed)
    
    cli.msg(chan, "Operation successful.")
    
    
@cmd("listfeeds", "listfeed")
@pmcmd("listfeeds", "listfeed")
def list_feeds(cli, nick, *rest):
    if rest and rest[0] == botconfig.CHANNEL:  # not pm
        chan = botconfig.CHANNEL
    else:
        chan = nick
    if not var.ENABLE_FEED:
        cli.notice(nick, "Feed is disabled.")
        return
    cli.msg(chan, "Feeds: " + ", ".join(var.FEEDS.values()))
        

def rss_daemon(cli):
    """Runs on a different thread and watches for rss feed updates"""
    with var.LOCK:
        conn = sqlite3.connect(botconfig.DATABASE_FILE)
        seen = dict((row[0],row[1]) for row in conn.execute('''select * from seen'''))
        conn.cursor().close()
        
    lnks = {}  # links found in each feed
    
    limit = var.MAX_FEED_UPDATES_AT_ONCE # limit to how many feed updates per thing
    counter = 0
    
    for fd in var.FEEDS:
        try:
            dataa = urlopen(fd, timeout = 7).read().decode("utf_8", "replace")
        except HTTPError as e:
            print("Get feed {0} failed with error: {1}".format(var.FEEDS[fd], e))
            continue
        except socket.error as e:
            print("Get feed {0} failed with error: {1}".format(var.FEEDS[fd], e))
            continue
        except socket.timeout as e:
            print("Get feed {0} timeout!".format(var.FEEDS[fd]))
            continue
        lnks[fd] = []
        parsed = feedparser.parse(dataa)
        for item in parsed["items"]:
            lnks[fd].append(item["link"])
            if item["link"].strip() in seen:  # seen + in database
                continue
            if item["link"].strip() in var.RECENT_LINKS:
                continue  # cached

            var.RECENT_LINKS.append(item["link"].strip())
            if len(var.RECENT_LINKS) > var.MAX_RECENT_LINKS:
                var.RECENT_LINKS.pop(0)

            # item was not in database/cache, so broadcast it!
            counter += 1
            if var.BITLY_USERNAME and var.BITLY_API_KEY:
                req = urlopen(SHORTEN_URL.format(quote(item["link"].strip()), 
                          login=var.BITLY_USERNAME, apikey=var.BITLY_API_KEY))
                data = req.read().decode("utf-8", "replace")
                url = json.JSONDecoder().decode(data).get("data", {}).get("url", {})
            else:
                url = item["link"].strip()
            
            if counter <= limit and fd in var.INITED_FEEDS:
                cli.msg(botconfig.CHANNEL, "\x01ACTION \x0314[{0}] {1}: {2}\x03\x01".format(
                    var.FEEDS[fd], item["title"], url))
            with var.LOCK:
                conn = sqlite3.connect(botconfig.DATABASE_FILE)
                conn.execute('''insert or ignore into seen values (?,?)''', (item["link"].strip(),fd))
                conn.cursor().close()
            
        if parsed["items"]:  # there were stuff, therefore feed is set
            var.INITED_FEEDS.add(fd)
        
    # seen dict: { url , feed_url }
    for linkk in seen:  # clear the db table 'seen'
        if ((lnks.get(seen[linkk]) and linkk not in lnks[seen[linkk]]) or
             seen[linkk] not in var.FEEDS):  # got deleted!
            print("clearing",linkk.encode(locale.getpreferredencoding(), "replace"))
            with var.LOCK:
                conn = sqlite3.connect(botconfig.DATABASE_FILE)
                conn.execute('''delete from seen where url=?''', (linkk,))
                conn.cursor().close()
    
    with var.LOCK:
        conn.commit()
        conn.cursor().close()
        
    t = threading.Timer(var.FEED_REFRESH_TIME, rss_daemon, [cli])
    t.daemon = True
    t.start()
    
    var.RSS_THREAD = t
    
var.RSS_THREAD = None

User = collections.namedtuple("User", ["acc"])
        
def connect_callback(cli):
    if '--nofeed' in sys.argv:
        var.ENABLE_FEED = False
    if feedparser and var.ENABLE_FEED:
        print("Starting rss reader...")
        thr = threading.Timer(var.FEED_REFRESH_TIME, rss_daemon, [cli])
        thr.daemon = True
        thr.start()
            
        var.RSS_THREAD = thr
    var.USERS = dict()  # nick : User
    
    @hook("whospcrpl", hookid=294)
    def on_whoreply(cli, server, nick, ident, cloak, user, status, acc):
        if user in var.USERS: return  # Don't add someone who is already there
        if user == botconfig.NICK:
            cli.nickname = user
            cli.ident = ident
            cli.hostmask = cloak
        if acc == "0":
            acc = "*"
        var.USERS[user] = User(acc)  # not logged in yet!
        
    @hook("endofwho", hookid=294)
    def afterwho(*args):
        unhook(294)
        
    cli.who(botconfig.CHANNEL, "%nuhaf")
